[8주차/오스카] 워크북 제출합니다.#84
Conversation
| const lps = data?.pages.flatMap((page) => page.data) ?? []; | ||
|
|
||
| // 스크롤 이벤트 등록 — raw 이벤트는 매우 빠르게 발생하므로 state만 업데이트 | ||
| useEffect(() => { |
There was a problem hiding this comment.
현재 구조는 스크롤이 조금만 움직여도 setScrollY가 실행되어 HomePage 컴포넌트 전체가 여러 번 리렌더링되는 현상이 발생할 것 같습니다
또한, 스크롤할 때마다 실시간으로 상태가 바뀌다 보니 useThrottle은 컴포넌트가 다 재생성된 후에 뒤늦게 작동하여 최적화 역할을 못할 것 같습니다
스크롤 이벤트 핸들러 함수 자체를 Throttling으로 감싸서, setScrollY가 호출되는 빈도 자체를 일정시간에 한 번으로 제한 하는 것이 좋을 것 같습니다
| fetchNextPage, | ||
| hasNextPage, | ||
| isFetchingNextPage, | ||
| } = isSearching ? searchQuery_ : allLpsQuery; |
There was a problem hiding this comment.
Debounce를 사용해서 검색 기능을 정석대로 잘 구현하신 것 같습니다. 300ms 내의 요청을 묶어서 요청으로 전달한다는 점, enabled로 빈값일때는 호출을 막은점에서 이번 워크북을 잘 이해하신 것 같아요
하지만 리엑트에서 훅은 if 문에 따라서 실행되지 않습니다.
자세히 설명하자만 if가 Ture거나 False일때 특정 훅이 실행되어 결과를 반환하는 것이 아닌
훅을 미리 실행해서 결과를 이미 받아온 후에 if 값에 맞춰서 미리 만들어진 결과를 전달하는 형식으로 동작합니다.
따라서 현재 isSearching ? searchQuery_ : allLpsQuery; 이 삼항 연산자 기준 화면은 잘 동작하겠지만
useSearchLps에 enabled 속성이 있음에도 검색 중 sort를 전환하면 두 훅의 query key가 동시에 변하면서 API 호출이 2번씩 일어나는 문제가 있을 것으로 보입니다.
또한 결국에 allLpsQuery도 상술 한 것처럼 실행되어있기에 사용자가 브라우저에 이탈 후 재 진입 시 브라우저는 기본적으로 모든 활성화 훅을 새로 고침 하기에 이때도 호출이 2번 일어납니다
const allLpsQuery = useInfiniteQuery({
queryKey: ['lps', sort],
queryFn: ({ pageParam }) => getLps(sort, pageParam as number, 20),
initialPageParam: 0,
getNextPageParam: (lastPage) => (lastPage.hasNext ? lastPage.nextCursor : undefined),
staleTime: 1000 * 60 * 3,
gcTime: 1000 * 60 * 10,
});이 부분을 useSearchLps의 query를 필수 값에서 선택 값으로 전환하고 병합해서
##useSearchLps
export function useSearchLps(query: string | undefined, sort: SortOrder) {
const isSearching = !!query?.trim();
return useInfiniteQuery({
queryKey: ['search', query || '', sort], #쿼리가 없어도 빈값으로 설정되어 정상동작합니다
queryFn: ({ pageParam }) => {
if (isSearching) { //훅 내부에서 검색 유무에 따라 API 호출을 처리합니다.
return searchLps(query!, sort, pageParam as number, 20);
}
return getLps(sort, pageParam as number, 20);
},
initialPageParam: 0,
getNextPageParam: (lastPage) => (lastPage.hasNext ? lastPage.nextCursor : undefined),
//enabled 사용은 훌륭했지만 두개의 훅을 하나로 합치려면 제거해야 검색어가 없을때 전체 화면이 보입니다
staleTime: 1000 * 60 * 3,
gcTime: 1000 * 60 * 10,
});
}현재 코드도 단일 책임 원칙이라는 측면에서 전체 결과와 검색 결과를 나눈 점은 훌륭합니다.
하지만 구조적 안정성과 네트워크 비용 절감을 위하여 이 방향을 고려하는것은 어떠신가요?
|
|
||
| const lps = data?.pages.flatMap((page) => page.data) ?? []; | ||
|
|
||
| const onIntersect = useCallback( |
There was a problem hiding this comment.
사용지가 검색을 진행 할 때 debounce로 인하여 300ms 까지 isSearching가 변하지 않아 이때는 괜찮지만
debounce의 300ms 지연 시간이 끝나 값이 반영된다면 위의 리뷰에서 말했던 삼항 연산자로 인해 변수들의 출처가 다른 훅으로 변환되게 됩니다. 따라서 onIntersect가 감시하는 주체가 달라지니 옵저버도 새로 만들어지게 됩니다.
이때 사용자가 추가적인 입력 시 300ms 동안 타이핑이 멈추었을 때 debounce로 인해 다시 반영 되며 옵저버의 생성과 파괴가 반복되게 되는 문제가 있을 것 같습니다.
위에서 전달한 피드백을 수용하신다면 두 개의 훅이 하나로 병합되었기에 Intersect가 감시하는 변수의 원천 메모리 주소가 변하지 않으므로 옵저버의 반복 생성과 파괴 현상도 제거 될 것 입니다
| const lps = data?.pages.flatMap((page) => page.data) ?? []; | ||
|
|
||
| // 스크롤 이벤트 등록 — raw 이벤트는 매우 빠르게 발생하므로 state만 업데이트 | ||
| useEffect(() => { |
There was a problem hiding this comment.
현재 코드에서는 스크롤할 때마다 setScrollY가 호출되면서 컴포넌트가 리렌더링되는 것 같습니다. 스로틀을 300ms로 걸었지만, 스로틀은 throttledScrollY 값 업데이트를 늦출 뿐이고 scrollY state 자체는 스크롤마다 즉시 바뀌면서 리렌더링을 계속 유발하게 될 수 있어요!
사용자 스크롤
→ handleScroll 실행 (매우 빠름)
→ setScrollY 호출
→ 리렌더링 발생 ← 스로틀이 막지 못하는 부분
→ throttledScrollY는 300ms 후에 바뀜
이런 순서로 진행되게 됩니다.
따라서 useRef로 스크롤 위치를 추적하거나, IntersectionObserver를 쓰면 리렌더링 없이 무한스크롤을 구현할 수 있을 것 같습니다! 이번주도 수고하셨습니다!
✅ 워크북 체크리스트
✅ 컨벤션 체크리스트
📌 주안점